package net.ustc.javamemcached.bootstrap;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import net.ustc.javamemcached.exception.XmlFormatException;
import net.ustc.javamemcached.protocol.Protocol;
import net.ustc.javamemcached.protocol.Protocol.ParameterType;
import net.ustc.javamemcached.protocol.Protocols;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class BootStrap {
	private static Logger log = LoggerFactory.getLogger(BootStrap.class);

	private static DocumentBuilder domBuilder;

	/* The config result */
	private Map<ChannelBuffer, Protocol> protocols;
	private List<InetSocketAddress> servers;
	private Map<ChannelBuffer, Class> mappings;

	public static String classpath;

	/*
	 * the path of server.xml, mapping.xml, protocol.xml are configurated in
	 * config.xml. so we can find server, mapping, protocol through config.xml
	 */
	private final String CONFIG_FILE_NAME = "config";
	private final String SERVER_FILE_NAME = "server";
	private final String MAPPING_FILE_NAME = "mapping";
	private final String PROTOCOL_FILE_NAME = "protocol";
	
	private final String FILE_SUFFIX = ".xml";

	/** tag configs/configFile in config.xml */
	private final String CONFIG_TAG_CONFIG_FILE = "configFile";
	/** tag configs/configFile/name in config.xml */
	private final String CONFIG_TAG_NAME = "name";
	/** tag configs/configFile/path in config.xml */
	private final String CONFIG_TAG_PATH = "path";
	/** tag configs/configFile/fileName in config.xml */
	private final String CONFIG_TAG_FILE_NAME = "fileName";
	
	/** tag servers/server/ip in server.xml */
	private final String SERVER_TAG_IP = "ip";
	/** tag servers/server/port in server.xml */
	private final String SERVER_TAG_PORT = "port";

	/** tag mappings/mapping in mapping.xml */
	private final String MAPPING_TAG_MAPPING = "mapping";
	/** tag mappings/mapping/command in mapping.xml */
	private final String MAPPING_TAG_COMMAND = "command";
	/** tag mappings/mapping/class in mapping.xml */
	private final String MAPPING_TAG_CLASS = "class";
	
	/** tags protocols/protocolName in protocol.xml */
	private final String PROTOCOL_TAG_PROTOCOL_NAME = "protocolName";
	/** tags protocols/version in protocol.xml */
	private final String PROTOCOL_TAG_VERSION = "version";
	/** tags protocols/wordSpliter in protocol.xml */
	private final String PROTOCOL_TAG_WORD_SPLITER = "wordSpliter";
	/** tags protocols/lineSpliter in protocol.xml */
	private final String PROTOCOL_TAG_LINE_SPLITER = "lineSpliter";
	/** tags protocols/protocol in protocol.xml */
	private final String PROTOCOL_TAG_PROTOCOL = "protocol";
	/** tags protocols/protocol/client2server in protocol.xml */
	private final String PROTOCOL_TAG_CLIENT2SERVER = "client2server";
	/** tags protocols/protocol/server2client in protocol.xml */
	private final String PROTOCOL_TAG_SERVER2CLIENT = "server2client";
	/** tags protocols/protocol/client2server/command in protocol.xml */
	private final String PROTOCOL_TAG_COMMAND = "command";
	/** tags protocols/protocol/client2server/parameters in protocol.xml */
	private final String PROTOCOL_TAG_PARAMETERS = "parameters";
	/** tags protocols/protocol/client2server/parameters/parameter in protocol.xml */
	private final String PROTOCOL_TAG_PARAMETER = "parameter";
	/** tags protocols/protocol/client2server/parameters/parameter/type in protocol.xml */
	private final String PROTOCOL_TAG_TYPE = "type";
	/** tags protocols/protocol/client2server/parameters/parameter/name in protocol.xml */
	private final String PROTOCOL_TAG_NAME = "name";
	/** tags protocols/protocol/client2server/parameters/parameter/repeated in protocol.xml */
	private final String PROTOCOL_TAG_REPEATED = "repeated";
	/** tags protocols/protocol/client2server/hasData in protocol.xml */
	private final String PROTOCOL_TAG_HAS_DATA = "hasData";
	/** tags protocols/protocol/client2server/needBackup in protocol.xml */
	private final String PROTOCOL_TAG_NEED_BACKUP = "needBackup";
	/** tags protocols/protocol/server2client/hasResponseData in protocol.xml */
	private final String PROTOCOL_TAG_HAS_RESPONSE_DATA = "hasResponseData";

	public BootStrap() {
		try {
			classpath = this.getClass().getClassLoader().getResource("")
					.getFile();
			domBuilder = DocumentBuilderFactory.newInstance()
					.newDocumentBuilder();
			config();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String agrs[]){
		new BootStrap();
	}

	public void config() throws XmlFormatException {
		String configXmlFile = classpath + CONFIG_FILE_NAME + FILE_SUFFIX;
		log.info("reading {}", configXmlFile);
		Document configDoc = parseFile2Dom(new File(configXmlFile));
		/* root element is tag \<configs\> */
		Element root = configDoc.getDocumentElement();
		NodeList configFileNodeList = root.getChildNodes();
		Node configFileNode;
		for (int i = 0; i < configFileNodeList.getLength(); i++) {
			configFileNode = configFileNodeList.item(i);
			if (!CONFIG_TAG_CONFIG_FILE.equals(configFileNode.getNodeName())) {
				continue;
			} else {
				try {
					String name = getChildNodeTextContent(configFileNode,
							CONFIG_TAG_NAME);
					String path = getChildNodeTextContent(configFileNode,
							CONFIG_TAG_PATH);
					String fileName = getChildNodeTextContent(configFileNode,
							CONFIG_TAG_FILE_NAME);
					if (SERVER_FILE_NAME.equals(name.trim())) {
						this.configServer(new File(classpath + path.trim()
								+ fileName.trim()));
					} else if (MAPPING_FILE_NAME.equals(name.trim())) {
						this.configMapping(new File(classpath + path.trim()
								+ fileName.trim()));
					} else if (PROTOCOL_FILE_NAME.equals(name.trim())) {
						this.configProtocol(new File(classpath + path.trim()
								+ fileName.trim()));
					}
				} catch (NullPointerException e) {
					log.error("config failure...");
					throw new XmlFormatException(e.getMessage());
				} catch (XmlFormatException e) {
					log.error("the format of file[{}] is wrong!",
							e.getMessage());
					throw new XmlFormatException(e.getMessage());
				}
			}
		}
	}

	/**
	 * parse server.xml to a {@link InetSocketAddress} list
	 * @param file server.xml
	 * @return
	 * @throws XmlFormatException
	 */
	public List<InetSocketAddress> configServer(File file)
			throws XmlFormatException {
		log.info("begin configing server.xml: {}", file.getPath());
		List<InetSocketAddress> servers = new ArrayList<InetSocketAddress>();

		Document doc = parseFile2Dom(file);
		NodeList serverList = doc.getElementsByTagName(SERVER_FILE_NAME);
		for (int i = 0; i < serverList.getLength(); i++) {
			Node serverNode = serverList.item(i);
			String ip = getChildNodeTextContent(serverNode, SERVER_TAG_IP);
			String port = getChildNodeTextContent(serverNode, SERVER_TAG_PORT);
			InetSocketAddress address = new InetSocketAddress(ip, Integer.parseInt(port));
			servers.add(address);
			log.info("server [{}:{}]", address.getHostName(),
					String.valueOf(address.getPort()));
		}
		log.info("finish configing server.xml: {}", file.getPath());
		return servers;
	}

	/**
	 * parse mapping.xml to a map, which keys command, values class name
	 * @param file mapping.xml
	 * @return
	 * @throws XmlFormatException
	 */
	public Map<ChannelBuffer, String> configMapping(File file)
			throws XmlFormatException {
		log.info("begin configing mapping.xml: {}", file.getPath());
		Map<ChannelBuffer, String> mappings = new HashMap<ChannelBuffer, String>();

		Document doc = parseFile2Dom(file);
		NodeList mappingList = doc.getElementsByTagName(MAPPING_TAG_MAPPING);
		for (int i = 0; i < mappingList.getLength(); i++) {
			Node mappingNode = mappingList.item(i);
			String command = getChildNodeTextContent(mappingNode, MAPPING_TAG_COMMAND);
			String clazz = getChildNodeTextContent(mappingNode, MAPPING_TAG_CLASS);
			mappings.put(ChannelBuffers.copiedBuffer(command, Protocol.USASCII), clazz);
			log.info("mapping.xml [{} : {}]", command, clazz);
		}
		log.info("finish configing mapping.xml: {}", file.getPath());
		return mappings;
	}

	public Protocols configProtocol(File file) throws XmlFormatException {
		log.info("begin configing protocol.xml: {}", file.getPath());
		Map<ChannelBuffer, Protocol> protocolMap = new HashMap<ChannelBuffer, Protocol>();

		Document doc = parseFile2Dom(file);
		Node protocols = doc.getDocumentElement();
		String protocolName = getChildNodeTextContent(protocols, PROTOCOL_TAG_PROTOCOL_NAME);
		String version = getChildNodeTextContent(protocols, PROTOCOL_TAG_VERSION);
		String wordSpliter = getChildNodeTextContent(protocols, PROTOCOL_TAG_WORD_SPLITER);
		String lineSpliter = getChildNodeTextContent(protocols, PROTOCOL_TAG_LINE_SPLITER);
		
		NodeList protocolList = doc.getElementsByTagName(PROTOCOL_TAG_PROTOCOL);
		for (int i = 0; i < protocolList.getLength(); i++) {
			Node protocol = protocolList.item(i);
			Node client2server = getChildNode(protocol, PROTOCOL_TAG_CLIENT2SERVER);
			Node server2client = getChildNode(protocol, PROTOCOL_TAG_SERVER2CLIENT);
			String commandString = getChildNodeTextContent(client2server, PROTOCOL_TAG_COMMAND);
			ChannelBuffer command = ChannelBuffers.copiedBuffer(commandString, Protocol.USASCII);
			Node parametersNode = getChildNode(client2server, PROTOCOL_TAG_PARAMETERS);
			List<Node> parameterList = getChildNodeList(parametersNode, PROTOCOL_TAG_PARAMETER);
			
			Map<String, Protocol.Parameter> parameters = new HashMap<String, Protocol.Parameter>();
			int index = 0;
			for(Node parameterNode: parameterList){
				index++;
				String parameterName = getChildNodeTextContent(parameterNode, PROTOCOL_TAG_NAME);
				String parameterType = getChildNodeTextContent(parameterNode, PROTOCOL_TAG_TYPE);
				ParameterType type = ParameterType.valueOf(parameterType);
				String parameterRepeated = getChildNodeTextContent(parameterNode, PROTOCOL_TAG_REPEATED);
				boolean repeated = Boolean.valueOf(parameterRepeated);
				parameters.put(parameterName, new Protocol.Parameter(parameterName, type, repeated, index));
			}
			
			String hasDataString = getChildNodeTextContent(client2server, PROTOCOL_TAG_HAS_DATA);
			boolean hasData = Boolean.valueOf(hasDataString);
			String needBackupString = getChildNodeTextContent(client2server, PROTOCOL_TAG_NEED_BACKUP);
			boolean needBackup = Boolean.valueOf(needBackupString);
			
			String hasResponseDataString = getChildNodeTextContent(server2client, PROTOCOL_TAG_HAS_RESPONSE_DATA);
			boolean hasResponseData = Boolean.valueOf(hasResponseDataString);
			
			Protocol p = new Protocol(command, parameters, needBackup, hasData, hasResponseData);
			protocolMap.put(command, p);
		}
		log.info("finish configing protocol.xml: {}", file.getPath());
		return new Protocols(protocolName, version, wordSpliter, lineSpliter, protocolMap);
	}

	/**
	 * parse a file to a {@link Document} object
	 * 
	 * @param file
	 * @return a Document object
	 */
	public static Document parseFile2Dom(File file) {
		domBuilder.reset();
		Document doc = null;
		try {
			doc = domBuilder.parse(new FileInputStream(file));
		} catch (SAXException e) {
			log.error("Parse file[{}] failure: {}", file.getName(),
					e.getMessage());
			System.exit(1);
		} catch (IOException e) {
			log.error("Load file[{}] failure: {}", file.getName(),
					e.getMessage());
			System.exit(1);
		}
		return doc;
	}
	
	/**
	 * get the text content of {@code node} whose name is {@code nodeName}
	 * 
	 * @param node
	 * @param nodeName
	 * @return
	 * @throws XmlFormatException
	 */
	public String getChildNodeTextContent(Node node, String nodeName)
			throws XmlFormatException {
		if (node == null || nodeName == null)
			return null;

		String text = null;
		NodeList childs = node.getChildNodes();
		for (int i = 0; i < childs.getLength(); i++) {
			Node tmpNode = childs.item(i);
			if (nodeName.equals(tmpNode.getNodeName())) {
				text = tmpNode.getTextContent();
			}
		}
		if (text == null) {
			throw new XmlFormatException("no " + nodeName + "exsits in "
					+ node.getNodeName());
		}
		return text;
	}
	
	/**
	 * get the node of {@code node} whose name is {@code nodeName}
	 * @param node
	 * @param nodeName
	 * @return
	 * @throws XmlFormatException
	 */
	public Node getChildNode(Node node, String nodeName)
			throws XmlFormatException {
		if (node == null || nodeName == null)
			return null;

		Node childNode = null;
		NodeList childs = node.getChildNodes();
		for (int i = 0; i < childs.getLength(); i++) {
			Node tmpNode = childs.item(i);
			if (nodeName.equals(tmpNode.getNodeName())) {
				childNode = tmpNode;
			}
		}
		if (childNode == null) {
			throw new XmlFormatException("no " + nodeName + "exsits in "
					+ node.getNodeName());
		}
		return childNode;
	}
	
	/**
	 * get the node list of {@code node} whose name is {@code nodeName}
	 * @param node
	 * @param nodeName
	 * @return
	 * @throws XmlFormatException
	 */
	public List<Node> getChildNodeList(Node node, String nodeName)
			throws XmlFormatException {
		if (node == null || nodeName == null)
			return null;

		List<Node> childNodeList = new ArrayList<Node>();
		
		NodeList childs = node.getChildNodes();
		for (int i = 0; i < childs.getLength(); i++) {
			Node tmpNode = childs.item(i);
			if (nodeName.equals(tmpNode.getNodeName())) {
				childNodeList.add(tmpNode);
			}
		}
		if (childNodeList.size() == 0) {
			throw new XmlFormatException("no " + nodeName + "exsits in "
					+ node.getNodeName());
		}
		return childNodeList;
	}
}
